﻿using System;
using System.ComponentModel;
using System.Globalization;
using Notoric.Commanding.CommandBase;

namespace Notoric.ViewModel.Elements.CWNotation
{
    /// <summary>
    /// Adapts a <see cref="Model.Elements.CWNotation.CoreSymbol"/> instance to a Windows Presentation Foundation view.
    /// </summary>
    public abstract class CoreSymbol : ViewModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="CoreSymbol"/> instance.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="adaptedElement">
        /// <para>
        /// Type: <see cref="Model.Elements.CWNotation.CoreSymbol"/>
        /// </para>
        /// <para>
        /// The adapted model element instance.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="adaptedElement"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="NullReferenceException">
        /// <paramref name="owner"/> is <see langword="null"/>.
        /// The values of the <see cref="ViewModelElement.UndoStack"/> and <see cref="ViewModelElement.RedoStack"/> properties
        /// are passed directly to the base class (<see cref="ViewModelElement"/>) constructor. Thus, there is not
        /// an occasion to react with a more appropriate exception.
        /// </exception>
        protected CoreSymbol(Staff owner, Model.Elements.CWNotation.CoreSymbol adaptedElement)
            : base(adaptedElement, owner, owner.UndoStack, owner.RedoStack)
        {
            RegisterToOwnerPropertyChanged<Staff>();
            RegisterToOwnerPropertyChanged<System>();
        }

        #endregion

        #region ViewModelElement Members

        /// <inheritdoc/>
        public override void Dispose()
        {
            UnregisterFromOwnerPropertyChanged<Staff>();
            UnregisterFromOwnerPropertyChanged<System>();
            base.Dispose();
        }

        #endregion

        #region Event Handlers

        /// <inheritdoc/>
        protected override bool AdaptedElement_PropertyChanged(PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "StaffPosition")
                RaisePropertyChanged("VerticalPosition");

            return base.AdaptedElement_PropertyChanged(e);
        }

        /// <inheritdoc/>
        protected override void Owner_PropertyChanged(ViewModelElement owner, PropertyChangedEventArgs e)
        {
            if (owner is Staff)
            {
                if (e.PropertyName == "Top" || e.PropertyName == "SpaceSize")
                    RaisePropertyChanged("VerticalPosition");
            }
            else if (owner is System && e.PropertyName == "Left")
                RaisePropertyChanged("Position");
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates the correct view-model adapter for a given model core symbol.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The staff that will directly own the created object.
        /// </para>
        /// </param>
        /// <param name="adaptedElement">
        /// <para>
        /// Type: <see cref="Model.Elements.CWNotation.CoreSymbol"/>
        /// </para>
        /// <para>
        /// The model element for which the adapter is being created.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="CoreSymbol"/>
        /// </para>
        /// <para>
        /// The created view-model adapter-
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="owner"/> or <paramref name="adaptedElement"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The type of the <paramref name="adaptedElement"/> is unknown.
        /// </exception>
        public static CoreSymbol CreateAdapter(Staff owner, Model.Elements.CWNotation.CoreSymbol adaptedElement)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");
            if (adaptedElement == null)
                throw new ArgumentNullException("adaptedElement");

            if (adaptedElement is Model.Elements.CWNotation.Clef)
                return new Clef(owner, (Model.Elements.CWNotation.Clef)adaptedElement);
            else if (adaptedElement is Model.Elements.CWNotation.Rest)
                return new Rest(owner, (Model.Elements.CWNotation.Rest)adaptedElement);
            else if (adaptedElement is Model.Elements.CWNotation.Chord)
                return new Chord(owner, (Model.Elements.CWNotation.Chord)adaptedElement);
            else if (adaptedElement is Model.Elements.CWNotation.KeySignature)
                return new KeySignature(owner, adaptedElement as Model.Elements.CWNotation.KeySignature);
            else if (adaptedElement is Model.Elements.CWNotation.TimeSignature)
                return new TimeSignature(owner, adaptedElement as Model.Elements.CWNotation.TimeSignature);

            throw new ArgumentException("Unknown model element type!", "adaptedElement");
        }

        /// <summary>
        /// Creates an undoable command for setting the <see cref="Position"/> of the symbol.
        /// </summary>
        /// <param name="position">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The position (in device independent pixels) of the symbol (including the <see cref="PositionableElement.Left"/> value of the <see cref="System"/>).
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetPositionCommand(double position)
        {
            var modelPosition = (double)LengthConverter.ConvertBack(position - FindOwner<System>().Left, typeof(double), null, CultureInfo.CurrentCulture);

            return new SetterCommand<double>(this, "Position", modelPosition, UndoStack, RedoStack);
        }

        /// <summary>
        /// Creates an undoable command for setting the <see cref="StaffPosition"/> of the symbol.
        /// </summary>
        /// <param name="position">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The new staff position.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetStaffPositionCommand(int position)
        {
            return new SetterCommand<int>(this, "StaffPosition", position, UndoStack, RedoStack);
        }

        /// <summary>
        /// Creates an undoable command for setting the <see cref="TypeSize"/> of the symbol.
        /// </summary>
        /// <param name="typeSize">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The new type size.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetTypeSizeCommand(double typeSize)
        {
            var modelSize = (double)LengthConverter.ConvertBack(typeSize, typeof(double), null, CultureInfo.CurrentCulture);

            return new SetterCommand<double>(this, "TypeSize", modelSize, UndoStack, RedoStack);
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the effective type size of the symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The effective type size (in device independent pixels) of the symbol.
        /// </para>
        /// </value>
        public double EffectiveTypeSize
        {
            get 
            {
                if (double.IsNaN(TypeSize))
                    return (double)LengthConverter.Convert(((Model.Elements.CWNotation.CoreSymbol)AdaptedElement).EffectiveTypeSize, typeof(double), null, CultureInfo.CurrentCulture);
                else
                    return TypeSize;
            }
        }

        /// <summary>
        /// Gets or sets the value indicating whether the symbol is currently selected.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Boolean"/>
        /// </para>
        /// <para>
        /// Indicates whether the symbol is currently selected.
        /// </para>
        /// </value>
        public bool IsSelected
        {
            get { return isSelected; }
            set
            {
                isSelected = value;

                RaisePropertyChanged("IsSelected");
            }
        }

        /// <summary>
        /// Gets or sets the position of the symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The offset (in device independent pixels) of the symbol from the left edge of the section interior.
        /// </para>
        /// </value>
        public double Position
        {
            get
            {
                return (double)LengthConverter.Convert(GetPropertyValue<double>("Position"), typeof(double), null, CultureInfo.CurrentCulture) +
                    FindOwner<System>().Left;
            }
            set
            {
                if (UnsavedChanges.ContainsKey("Position"))
                {
                    var cmd = (SetterCommand<double>)UnsavedChanges["Position"];
                    cmd.Value = (double)LengthConverter.ConvertBack(value - FindOwner<System>().Left, typeof(double), null, CultureInfo.CurrentCulture);
                }
                else
                    UnsavedChanges["Position"] = CreateSetPositionCommand(value);

                RaisePropertyChanged("Position");
            }
        }

        /// <summary>
        /// Gets or sets the staff position of the symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The staff position of the symbol.
        /// </para>
        /// </value>
        public int StaffPosition
        {
            get { return GetPropertyValue<int>("StaffPosition"); }
            set
            {
                if (UnsavedChanges.ContainsKey("StaffPosition"))
                {
                    var cmd = (SetterCommand<int>)UnsavedChanges["StaffPosition"];
                    cmd.Value = value;
                }
                else
                    UnsavedChanges["StaffPosition"] = CreateSetStaffPositionCommand(value);

                RaisePropertyChanged("StaffPosition");
                RaisePropertyChanged("VerticalPosition");
            }
        }

        /// <summary>
        /// Gets or sets the type size of the symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The type size (in device independent pixels) of the symbol.<br/>
        /// The value <see cref="Double.NaN"/> expresses standard size (default).
        /// </para>
        /// </value>
        public double TypeSize
        {
            get
            {
                double modelSize = GetPropertyValue<double>("TypeSize");

                return (double)LengthConverter.Convert(modelSize, typeof(double), null, CultureInfo.CurrentCulture);
            }
            set
            {
                if (UnsavedChanges.ContainsKey("TypeSize"))
                {
                    var cmd = (SetterCommand<double>)UnsavedChanges["TypeSize"];
                    cmd.Value = (double)LengthConverter.ConvertBack(value, typeof(double), null, CultureInfo.CurrentCulture);
                }
                else
                    UnsavedChanges["TypeSize"] = CreateSetTypeSizeCommand(value);

                RaisePropertyChanged("TypeSize");
                RaisePropertyChanged("FontSize");
            }
        }

        /// <summary>
        /// Gets the vertical position of the symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The offset (in device independent pixels) of the symbol
        /// from the top section's edge.
        /// </para>
        /// </value>
        public double VerticalPosition
        {
            get { return FindOwner<Staff>().Bottom - StaffPosition * (FindOwner<Staff>().SpaceSize / 2); }
        }

        /// <summary>
        /// Gets the width of the symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The width (in device independent pixels) of the symbol.
        /// </para>
        /// </value>
        public double Width
        {
            get { return (double)LengthConverter.Convert(((Model.Elements.CWNotation.CoreSymbol)AdaptedElement).Width, typeof(double), null, CultureInfo.CurrentCulture); }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="IsSelected"/> property.
        /// </summary>
        private bool isSelected;

        #endregion
    }
}
